<?php

/**
 * CCoreMimePart
 * CCoreMimePartsCollection
 *
 * @author Usenko Timur <usenko.timur@gmail.com>
 * @package mime
 */

CCore::Using('base.collection');
CCore::Using('mime.header');
CCore::Using('mime.utils');
CCore::Using('mime.callbacks.memory');
CCore::Using('mime.stream');

/**
 * @package mime
 */
class CCoreMimePart
{
	/**
	 * @var string
	 */
	static $DefaultCharset = C_MIME_CHARSET_UTF8;

	/**
	 * @var string
	 */
	static $ForceCharset = null;

	#<editor-fold defaultstate="collapsed" desc="constants">
	const POS_HEADERS = 1;
	const POS_BODY = 2;
	const POS_SUBPARTS = 3;
	const POS_CLOSE_BOUNDARY = 4;
	#</editor-fold>

	#<editor-fold defaultstate="collapsed" desc="public">
	/**
	 * @var CCoreMimeHeadersCollection
	 */
	public $Headers;

	/**
	 * @var resourse
	 */
	public $Body;

	/**
	 * @var CCoreMimePartsCollection
	 */
	public $SubParts;

	/**
	 * @var string
	 */
	public $Boundary;

	/**
	 * @var array
	 */
	public $LineParts;
	#</editor-fold>

	#<editor-fold defaultstate="collapsed" desc="protected">
	/**
	 * @var int
	 */
	protected $iParseBuffer;

	/**
	 * @var string
	 */
	protected $sCharset;
	#</editor-fold>

	/**
	 * @return CCoreMimePart
	 */
	public function __construct()
	{
		$this->iParseBuffer = 8192;
		$this->Reset();
	}

	/**
	 * @return void
	 */
	public function Reset()
	{
		$this->Headers = new CCoreMimeHeadersCollection();
		$this->Body = fopen('php://temp/maxmemory:1048576', 'r+b');
		//$this->Body = fopen('php://memory', 'r+b');
		$this->SubParts = new CCoreMimePartsCollection();
		$this->LineParts = array();
		$this->Boundary = '';
		$this->sCharset = '';
	}

	/**
	 * @return string
	 */
	public function GetCharset()
	{
		return (empty($this->sCharset)) ? CCoreMimePart::$DefaultCharset : $this->sCharset;
	}

	/**
	 * @param string $sCharset
	 * @return void
	 */
	public function SetCharset($sCharset)
	{
		$this->sCharset = $sCharset;
	}

	/**
	 * @param int $iParseBuffer
	 * @return void
	 */
	public function SetParseBuffer($iParseBuffer)
	{
		$this->iParseBuffer = $iParseBuffer;
	}

	/**
	 * @param string $sFileName
	 * @param ICoreMimeCallbacks $oCallbackClass = null
	 * @return void
	 */
	public function ParseFromFile($sFileName, ICoreMimeCallbacks $oCallbackClass = null)
	{
		$rStreamHandle = (@file_exists($sFileName)) ? @fopen($sFileName, 'rb') : false;
		if (is_resource($rStreamHandle))
		{
			$this->ParseFromStream($rStreamHandle, $oCallbackClass);
			@fclose($rStreamHandle);
		}
	}

	/**
	 * @param string $sRawMessage
	 * @param ICoreMimeCallbacks $oCallbackClass = null
	 * @return void
	 */
	public function ParseFromString($sRawMessage, ICoreMimeCallbacks $oCallbackClass = null)
	{
		$rStreamHandle = (0 < strlen($sRawMessage)) ? @fopen('php://memory', 'r+b') : false;
		if (is_resource($rStreamHandle))
		{
			@fwrite($rStreamHandle, $sRawMessage);
			unset($sRawMessage);
			@fseek($rStreamHandle, 0);
			$this->ParseFromStream($rStreamHandle, $oCallbackClass);
			@fclose($rStreamHandle);
		}
	}

	/**
	 * @param resource $rStreamHandle
	 * @param ICoreMimeCallbacks $oCallbackClass = null
	 * @return void
	 */
	public function ParseFromStream($rStreamHandle, ICoreMimeCallbacks $oCallbackClass = null)
	{
		$oCallbackClass = (null === $oCallbackClass) ? new CCoreMimeCallbacksMemory() : $oCallbackClass;

		$this->Reset();

		$bIsOef = false;
		$iOffset = 0;
		$sBuffer = '';
		$sPrevBuffer = '';
		$aBoundaryStack = array();
		
		$oCallbackClass->StartParse($this);

		$this->LineParts[] =& $this;
		$this->ParseFromStreamRecursion($rStreamHandle, $oCallbackClass, $iOffset, $sPrevBuffer, $sBuffer, $aBoundaryStack, $bIsOef);

		$sFirstNotNullCharset = null;
		foreach ($this->LineParts as /* @var $oMimePart CCoreMimePart */ &$oMimePart)
		{
			$sCharset = $oMimePart->Headers->GetCharsetHeaderValue();
			if (0 < strlen($sCharset))
			{
				$sFirstNotNullCharset = $sCharset;
				break;
			}
		}

		$sForceCharset = self::$ForceCharset;
		if (0 < strlen($sForceCharset))
		{
			foreach ($this->LineParts as /* @var $oMimePart CCoreMimePart */ &$oMimePart)
			{
				$oMimePart->SetCharset($sForceCharset);
			}
		}
		else
		{
			$sFirstNotNullCharset = (null !== $sFirstNotNullCharset)
				? $sFirstNotNullCharset : self::$DefaultCharset;

			foreach ($this->LineParts as /* @var $oMimePart CCoreMimePart */ &$oMimePart)
			{
				$sHeaderCharset = $oMimePart->Headers->GetCharsetHeaderValue();
				$oMimePart->SetCharset((0 < strlen($sHeaderCharset)) ? $sHeaderCharset : $sFirstNotNullCharset);
			}
		}

		$oCallbackClass->EndParse($this);
	}

	/**
	 * @param resource $rStreamHandle
	 * @return void
	 */
	public function ParseFromStreamRecursion($rStreamHandle, ICoreMimeCallbacks &$oCallbackClass, &$iOffset, &$sPrevBuffer, &$sBuffer, &$aBoundaryStack, &$bIsOef, $bNotFirstRead = false)
	{
		$oCallbackClass->StartParseMimePart($this);
		
		$iPos = 0;
		$iParsePosition = self::POS_HEADERS;
		$sCurrentBoundary = '';
		$aHeadersLines = array();
		while (true)
		{
			if (!$bNotFirstRead)
			{
				$sPrevBuffer = $sBuffer;
				$sBuffer = '';
			}
			
			if (!$bIsOef && !feof($rStreamHandle))
			{
				if (!$bNotFirstRead)
				{
					$sBuffer = @fread($rStreamHandle, $this->iParseBuffer);
					if (false === $sBuffer)
					{
						break;
					}

					$oCallbackClass->ReadBuffer($sBuffer);
				}
				else
				{
					$bNotFirstRead = false;
				}
			}
			else if ($bIsOef && empty($sBuffer))
			{
				break;
			}
			else
			{
				$bIsOef = true;
			}

			while (true)
			{
				$sCurrentLine = $sPrevBuffer.$sBuffer;
				if (self::POS_HEADERS === $iParsePosition)
				{
					$iEndLen = 4;
					$iPos = strpos($sCurrentLine, "\r\n\r\n", $iOffset);
					if (false === $iPos)
					{
						$iEndLen = 2;
						$iPos = strpos($sCurrentLine, "\n\n", $iOffset);
					}

					if (false !== $iPos)
					{
						$aHeadersLines[] = substr($sCurrentLine, $iOffset, $iPos + $iEndLen - $iOffset);

						$this->Headers->Parse(implode($aHeadersLines));
						$aHeadersLines = array();

						$this->Headers->SetParent($this, true);

						$oCallbackClass->InitMimePartHeader();
						
						$sBoundary = $this->Headers->GetBoundaryHeaderValue();
						if (strlen($sBoundary) > 0)
						{
							$sBoundary = '--'.$sBoundary;
							$sCurrentBoundary = $sBoundary;
							array_unshift($aBoundaryStack, $sBoundary);
						}

						$iOffset = $iPos + $iEndLen;
						$iParsePosition = self::POS_BODY;
						continue;
					}
					else
					{
						$iBufferLen = strlen($sPrevBuffer);
						if ($iBufferLen > $iOffset)
						{
							$aHeadersLines[] = substr($sPrevBuffer, $iOffset);
							$iOffset = 0;
						}
						else
						{
							$iOffset -= $iBufferLen;
						}
						break;
					}
				}
				else if (self::POS_BODY === $iParsePosition)
				{
					$iPos = false;
					$sBoundaryLen = 0;
					$bIsBoundaryEnd = false;
					$bCurrentPartBody = false;
					$bIsBoundaryCheck = 0 < count($aBoundaryStack);

					foreach ($aBoundaryStack as $sKey => $sBoundary)
					{
						if (false !== ($iPos = strpos($sCurrentLine, $sBoundary, $iOffset)))
						{
							if ($sCurrentBoundary === $sBoundary)
							{
								$bCurrentPartBody = true;
							}

							$sBoundaryLen = strlen($sBoundary);
							if ('--' === substr($sCurrentLine, $iPos + $sBoundaryLen, 2))
							{
								$sBoundaryLen += 2;
								$bIsBoundaryEnd = true;
								unset($aBoundaryStack[$sKey]);
								$sCurrentBoundary = (isset($aBoundaryStack[$sKey + 1]))
									? $aBoundaryStack[$sKey + 1] : '';
							}
							break;
						}
					}

					if (false !== $iPos)
					{
						$oCallbackClass->WriteBody(substr($sCurrentLine, $iOffset, $iPos - $iOffset));
						$iOffset = $iPos;

						if ($bCurrentPartBody)
						{
							$iParsePosition = self::POS_SUBPARTS;
							continue;
						}

						$oCallbackClass->EndParseMimePart($this);
						return true;
					}
					else
					{
						$iBufferLen = strlen($sPrevBuffer);
						if ($iBufferLen > $iOffset)
						{
							$oCallbackClass->WriteBody(substr($sPrevBuffer, $iOffset));
							$iOffset = 0;
						}
						else
						{
							$iOffset -= $iBufferLen;
						}
						break;
					}
				}
				else if (self::POS_SUBPARTS === $iParsePosition)
				{
					$iPos = false;
					$sBoundaryLen = 0;
					$bIsBoundaryEnd = false;
					$bCurrentPartBody = false;
					$bIsBoundaryCheck = 0 < count($aBoundaryStack);

					foreach ($aBoundaryStack as $sKey => $sBoundary)
					{
						if (false !== ($iPos = strpos($sCurrentLine, $sBoundary, $iOffset)))
						{
							if ($sCurrentBoundary === $sBoundary)
							{
								$bCurrentPartBody = true;
							}

							$sBoundaryLen = strlen($sBoundary);
							if ('--' === substr($sCurrentLine, $iPos + $sBoundaryLen, 2))
							{
								$sBoundaryLen += 2;
								$bIsBoundaryEnd = true;
								unset($aBoundaryStack[$sKey]);
								$sCurrentBoundary = (isset($aBoundaryStack[$sKey + 1]))
									? $aBoundaryStack[$sKey + 1] : '';
							}
							break;
						}
					}
					
					if (false !== $iPos && $bCurrentPartBody)
					{
						$iOffset = $iPos + $sBoundaryLen;

						$oSubPart = new CCoreMimePart();
						$oSubPart->SetParseBuffer($this->iParseBuffer);
						$oSubPart->ParseFromStreamRecursion($rStreamHandle, $oCallbackClass,
							$iOffset, $sPrevBuffer, $sBuffer, $aBoundaryStack, $bIsOef, true);

						$this->SubParts->Add($oSubPart);
						$this->LineParts[] =& $oSubPart;
						//$iParsePosition = self::POS_HEADERS;
						unset($oSubPart);
					}
					else
					{
						$oCallbackClass->EndParseMimePart($this);
						return true;
					}
				}
			}
		}

		if (0 < strlen($sPrevBuffer))
		{
			if (self::POS_HEADERS === $iParsePosition)
			{
				$aHeadersLines[] = ($iOffset < strlen($sPrevBuffer))
					? substr($sPrevBuffer, $iOffset)
					: $sPrevBuffer;

				$this->Headers->Parse(implode($aHeadersLines));
				$aHeadersLines = array();

				$this->Headers->SetParent($this, true);

				$oCallbackClass->InitMimePartHeader();
			}
			else if (self::POS_BODY === $iParsePosition)
			{
				if (!$bIsBoundaryCheck)
				{
					$oCallbackClass->WriteBody(($iOffset < strlen($sPrevBuffer))
						? substr($sPrevBuffer, $iOffset) : $sPrevBuffer);
				}
			}
		}
		else
		{
			if (self::POS_HEADERS === $iParsePosition && 0 < count($aHeadersLines))
			{
				$this->Headers->Parse(implode($aHeadersLines));
				$aHeadersLines = array();

				$this->Headers->SetParent($this, true);
				
				$oCallbackClass->InitMimePartHeader();
			}
		}

		$oCallbackClass->EndParseMimePart($this);
		return true;
	}

	/**
	 * @return int
	 */
	public function EstimatedSize()
	{
		$fCoefficient = 1;
		$sContentTransferEncoding = $this->Headers->GetContentTransferEncodingValue();
		switch (strtolower($sContentTransferEncoding))
		{
			case 'base64':
				$fCoefficient = 0.75;
				break;
			case 'quoted-printable':
				$fCoefficient = 0.44;
				break;
		}

		$aStat = fstat($this->Body);
		return isset($aStat['size']) ? (int) ($aStat['size'] * $fCoefficient) : 0;
	}

	/**
	 * @return resourse
	 */
	public function GetBinaryBodyStream()
	{
		rewind($this->Body);

		$sFunctionName = CCoreMimeUtils::GetInlineDecodeFunctionName(
			$this->Headers->GetContentTransferEncodingValue());

		return CCoreMimeStreamWrapper::CreateBinaryStream($this->Body, $sFunctionName);
	}

	/**
	 * @return resourse
	 */
	public function GetDecodedBodyStream($sOutputCharset = C_MIME_CHARSET_UTF8)
	{
		rewind($this->Body);

		$sFunctionName = CCoreMimeUtils::GetInlineDecodeFunctionName(
			$this->Headers->GetContentTransferEncodingValue());

		return CCoreMimeStreamWrapper::CreateBinaryStream($this->Body, $sFunctionName,
			$this->GetCharset(), $sOutputCharset);
	}
	
	/**
	 * @todo
	 * @param bool $bThrowContentTypeHeader = false
	 * @return void
	 */
	public function PassthruBinaryBody($bThrowContentTypeHeader = false)
	{
		if ($this->Body)
		{
			if ($bThrowContentTypeHeader)
			{
				$sContentType = trim($this->Headers->GetContentTypeValue());
				#TODO: добавить определение типа по имени файла
				$sContentType = empty($sContentType) ? 'application/octet-stream' : $sContentType;
				@header('Content-Type: '.$sContentType);
			}

			$rStream = $this->GetBinaryBodyStream();

			fpassthru($rStream);
		}
	}
}

/**
 * @package mime
 */
class CCoreMimePartsCollection extends ACoreBaseCollection
{
	/**
	 * @return CCoreMimePartsCollection
	 */
	public function __construct()
	{
		parent::__construct();
	}

	/**
	 * @param CCoreMimePart $oMimePart
	 * @return void
	 */
	public function Add(CCoreMimePart $oMimePart)
	{
		CCore::CheckTypes(array('$oMimePart'));

		parent::Add($oMimePart);
	}
}
